Skip to content

feat(057): In-proxy profiles + permanent URLs (/mcp/p/<slug>)#622

Merged
Dumbris merged 4 commits into
mainfrom
057-in-proxy-profiles-impl
Jun 8, 2026
Merged

feat(057): In-proxy profiles + permanent URLs (/mcp/p/<slug>)#622
Dumbris merged 4 commits into
mainfrom
057-in-proxy-profiles-impl

Conversation

@Dumbris

@Dumbris Dumbris commented Jun 7, 2026

Copy link
Copy Markdown
Member

Summary

Implements Spec 057 — In-Proxy Profiles + Permanent URLs (GH #55).

What ships

  • Profile config entity (internal/config/profiles.go): ProfileConfig{Name, Servers}, slug validation (^[a-z0-9][a-z0-9_-]{0,62}$), reserved slug rejection (all/code/call/p), dup/unknown-server rules, EffectiveServers()
  • ProfileScope context (internal/profile/context.go): immutable request-scoped set, nil-receiver allow-all, AllowedServerNames() for T011b
  • /mcp/p/<slug> route (server.go): profileMiddleware registered as mcpAuthMiddleware(profileMiddleware(logging(handler))) — auth before profile; 404 JSON responses for no-profiles and unknown-slug
  • Four profile-boundary surfaces (all independent of enforceAgentScope):
  • Activity metadata: withProfileMeta(ctx, intentMap) injects `metadata["profile"]" at tool-call activity emit sites (FR-011)
  • OAS update: swagger.yaml updated with profiles config schema (T001-T007 commit)
  • Docs: docs/features/profiles.md

Tests

  • T012 mandatory regression: unauthenticated /mcp/p/<slug> is still filtered (profile check independent of AdminContext)
  • T013: 404-no-profiles, 404-unknown-slug, retrieve_tools isolation, /mcp full-union unchanged, call_tool_* out-of-profile rejection
  • T011a test: upstream_servers list filtered at profile URL
  • T015/T016: policy intersection — profile error names the profile, not the token
  • T018: per-server disabled_tools respected inside profile (guard test)
  • T019: activity metadata smoke test
  • T020: backward-compat — /mcp, /mcp/call, /mcp/code unaffected

Gates

  • go test ./internal/... -race
  • ./scripts/test-api-e2e.sh 65/65 ✅
  • ./scripts/run-linter.sh 0 issues ✅
  • Dual-edition build: go build ./cmd/mcpproxy + go build -tags server ./cmd/mcpproxy

Closes #55

Related #55

Dumbris and others added 3 commits June 7, 2026 18:16
Foundational phase for in-proxy profiles (Spec 057, GH #55). No behaviour
change yet — adds the config entity + validation and the request-scoped scope
primitive; the /mcp/p/ routing and filter sites come next.

- internal/config/profiles.go: ProfileConfig{Name,Servers} + ValidateProfiles
  (fatal: slug pattern ^[a-z0-9][a-z0-9_-]{0,62}$, reserved {all,code,call,p},
  duplicate name; warn-skip: unknown server, empty servers). EffectiveServers
  helper. (T003)
- internal/config/config.go: Config.Profiles []ProfileConfig `json:",omitempty"`
  after Servers (SC-004 byte-identical when absent); Validate() runs
  ValidateProfiles, fails on fatal, stashes warnings; ProfileWarnings() accessor
  for the boot path to log. (T004)
- internal/profile/context.go: ProfileScope{Name, servers set} with nil-receiver
  allow-all Allows(), WithProfileScope/ProfileScopeFromContext (mirrors
  internal/auth/context.go). (T007)
- Tests red-first: profiles_test.go (validation table + round-trip, T002/T005),
  profile/context_test.go (membership, nil allow-all, empty deny-all,
  context round-trip, T006).

Both editions build; go vet clean.

Related #55

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…T008-T022)

- profileMiddleware in server.go: resolves slug from config snapshot, builds
  ProfileScope, injects into context, 404s on missing/unknown profile
- /mcp/p/ and /mcp/p routes registered after mcpAuthMiddleware (auth->profile)
- retrieve_tools filter (T010): profile.Allows() independent of enforceAgentScope
- call_tool_* filter (T011): profile error before token-scope check, names profile
- upstream_servers list filter (T011a): excludes out-of-profile servers
- code_execution filter (T011b): profile servers intersected into AllowedServers
  so empty caller list at a profile URL does not mean 'allow all'
- ProfileScope.AllowedServerNames() helper for T011b intersection
- withProfileMeta() injects metadata["profile"] at activity emit sites (T019)
- Integration tests: 404-no-profiles, 404-unknown-slug, retrieve_tools isolation,
  full /mcp union unchanged, call_tool_* out-of-profile rejection, upstream_servers
  list filtering, policy intersection, per-server disabled_tools guard, activity
  metadata smoke-test, backward-compat endpoint reachability (T012-T018/T020)
- docs/features/profiles.md

Related #55
Copy config struct before mutating to avoid -race failures.

Related #55
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 7, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 698b02f
Status: ✅  Deploy successful!
Preview URL: https://236955be.mcpproxy-docs.pages.dev
Branch Preview URL: https://057-in-proxy-profiles-impl.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

codecov-commenter commented Jun 7, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 71.79487% with 44 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/config/profiles.go 62.16% 14 Missing ⚠️
internal/server/mcp_code_execution.go 35.71% 8 Missing and 1 partial ⚠️
internal/profile/context.go 70.83% 7 Missing ⚠️
internal/server/mcp.go 75.00% 7 Missing ⚠️
internal/runtime/event_bus.go 0.00% 3 Missing ⚠️
internal/config/config.go 50.00% 1 Missing and 1 partial ⚠️
internal/server/mcp_routing.go 0.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown

📦 Build Artifacts

Workflow Run: View Run
Branch: 057-in-proxy-profiles-impl

Available Artifacts

  • archive-darwin-amd64 (28 MB)
  • archive-darwin-arm64 (25 MB)
  • archive-linux-amd64 (16 MB)
  • archive-linux-arm64 (14 MB)
  • archive-windows-amd64 (28 MB)
  • archive-windows-arm64 (24 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (21 MB)
  • installer-dmg-darwin-arm64 (19 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 27101438678 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@Dumbris Dumbris enabled auto-merge (squash) June 7, 2026 15:57
…etadata)

Round-1 review fixes for the in-proxy profiles impl (MCP-1281/MCP-1278):

1. code_execution empty-effective-set bypass (MUST-FIX). A deny-all profile
   (servers: []) or a non-overlapping token∩profile yields an empty effective
   allow-list, which the JS runtime treated as "allow all" — leaking every
   server through code_execution. Add ExecutionOptions.RestrictToAllowed:
   when an active profile is in context, the allow-list is enforced even when
   empty (empty set = deny all). Set by the profile path in
   handleCodeExecution.

2. FR-011 metadata written to wrong shape (MUST-FIX). withProfileMeta smuggled
   the slug into the intent submap, so records landed as
   metadata.intent.profile and error paths dropped it entirely. Thread an
   explicit profile slug through EmitActivityToolCallCompleted →
   metadata["profile"] at top level, on success AND error paths.

3. Spec amendment (no code change): document per-request profile resolution
   (hot-reload takes effect on the next request) in spec.md + data-model.md,
   replacing the inaccurate "snapshot-until-reconnect" wording. Per-request
   re-resolution is kept as the safer behavior.

Tests: jsruntime deny-all/non-empty enforcement + backward-compat; Activity
service metadata["profile"] (success+error, not nested); code_execution
deny-all/non-overlapping rejection at the profile path; upgraded the FR-011
integration test to assert the persisted record's metadata["profile"].

Related #622

@mcpproxy-gatekeeper mcpproxy-gatekeeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gatekeeper approval — Codex review verdict: ACCEPT.

This approval is posted automatically by the MCPProxy Gatekeeper App on behalf of the Codex reviewer (verdict of record lives in the Paperclip review thread). Author≠approver satisfied; QA + CI gates enforced separately.

Auto-approved per Model B (MCP-1249).

@mcpproxy-gatekeeper mcpproxy-gatekeeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gatekeeper approval — Codex review verdict: ACCEPT.

This approval is posted automatically by the MCPProxy Gatekeeper App on behalf of the Codex reviewer (verdict of record lives in the Paperclip review thread). Author≠approver satisfied; QA + CI gates enforced separately.

Auto-approved per Model B (MCP-1249).

@mcpproxy-gatekeeper mcpproxy-gatekeeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gatekeeper approval — Codex review verdict: ACCEPT.

This approval is posted automatically by the MCPProxy Gatekeeper App on behalf of the Codex reviewer (verdict of record lives in the Paperclip review thread). Author≠approver satisfied; QA + CI gates enforced separately.

Auto-approved per Model B (MCP-1249).

@Dumbris Dumbris merged commit 3a7718e into main Jun 8, 2026
47 checks passed
@Dumbris Dumbris deleted the 057-in-proxy-profiles-impl branch June 8, 2026 15:07
Dumbris added a commit that referenced this pull request Jun 8, 2026
… (MCP-1249)

Hands-off half of Model B: a launchd timer runs gatekeeper-sweep.sh every 5m,
which invokes gatekeeper-approve.sh for each open PR. Codex ACCEPT -> App
approval -> GitHub auto-merge, no human, no admin.

- gatekeeper-approve.sh hardened to be safe under repeated/unattended runs:
  - no-op if PR is closed/merged
  - stale-verdict guard: only approve the exact SHA Codex reviewed (head moved
    past it -> skip, re-review needed) [exit 6]
  - idempotency: skip if the Gatekeeper already approved the current head
  - resolver now also extracts the reviewer-pinned SHA
- gatekeeper-sweep.sh: bash-3.2 safe (macOS /bin/bash, no mapfile); skips
  quietly when unconfigured or Paperclip unreachable; timestamped logging.
- app.mcpproxy.gatekeeper.sweep.plist: launchd template (StartInterval 300,
  RunAtLoad, background/low-IO) + install/enable/disable instructions.

Validated: dry sweep over 14 open PRs no-ops correctly; idempotent no-op on the
already-merged #622; timer loaded and first run logged.

Related MCP-1249
Dumbris added a commit that referenced this pull request Jun 15, 2026
* ci: gatekeeper App auto-approver bridge scaffold (MCP-1249)

Bridge that turns a Paperclip Codex review verdict of ACCEPT into a real
GitHub approving review posted by the "MCPProxy Gatekeeper" GitHub App, so
the required-1-approving-review branch protection is satisfied without an
admin override and without the author approving their own PR (author!=approver).
Pairs with scripts/arm-auto-merge.sh for full Model B.

- Reads verdict of record from the Paperclip review thread (bots don't post
  to GitHub); only ACCEPT approves, request_changes/unknown are no-ops.
- Mints a GitHub App installation token (RS256 JWT via openssl, no extra deps).
- Inert until configured (GATEKEEPER_APP_ID / _INSTALLATION_ID / _PRIVATE_KEY,
  e.g. ~/.mcpproxy-gatekeeper/env) — exits 2 with setup guidance.
- --dry-run + --verdict overrides for testing.

Validated against live data: reads accept for PR #622, fails safe unconfigured,
no-ops on request_changes. App registration + cred wiring is the remaining
(owner) step.

Related MCP-1249

* ci: gatekeeper auto-trigger — sweep + launchd timer, hardened approve (MCP-1249)

Hands-off half of Model B: a launchd timer runs gatekeeper-sweep.sh every 5m,
which invokes gatekeeper-approve.sh for each open PR. Codex ACCEPT -> App
approval -> GitHub auto-merge, no human, no admin.

- gatekeeper-approve.sh hardened to be safe under repeated/unattended runs:
  - no-op if PR is closed/merged
  - stale-verdict guard: only approve the exact SHA Codex reviewed (head moved
    past it -> skip, re-review needed) [exit 6]
  - idempotency: skip if the Gatekeeper already approved the current head
  - resolver now also extracts the reviewer-pinned SHA
- gatekeeper-sweep.sh: bash-3.2 safe (macOS /bin/bash, no mapfile); skips
  quietly when unconfigured or Paperclip unreachable; timestamped logging.
- app.mcpproxy.gatekeeper.sweep.plist: launchd template (StartInterval 300,
  RunAtLoad, background/low-IO) + install/enable/disable instructions.

Validated: dry sweep over 14 open PRs no-ops correctly; idempotent no-op on the
already-merged #622; timer loaded and first run logged.

Related MCP-1249

* fix(gatekeeper): fail-closed SHA guard — refuse approval without a matching reviewed SHA (MCP-1249)

Codex REQUEST_CHANGES (PR #687): the stale-SHA guard in gatekeeper-approve.sh
ran only `if [[ -n "$REVIEWED_SHA" && ... ]]`, so a verdict that pinned NO SHA
(REVIEWED_SHA empty) — or the manual --verdict override path, which always
yielded an empty SHA — bypassed the check and auto-approved the PR's CURRENT
head. An old ACCEPT could thus approve unreviewed code after a post-review
force-push.

Fix (fail-closed):
- Make the SHA checks UNCONDITIONAL. No reviewed SHA resolvable -> REFUSE
  (exit 7, explicit message); reviewed SHA != head -> REFUSE (exit 6, stale);
  only reviewed SHA == head approves.
- Add --reviewed-sha to pair with --verdict so the manual override is held to
  the same fail-closed requirement (no blind manual approvals either).
- Document the invariant + new exit code 7 in the header.

Adds scripts/gatekeeper-approve.test.sh: hermetic (stubs gh, --dry-run, no
network/GitHub) regression covering all four Codex acceptance cases. Red on the
old script (case 1 approved blind), green on the fix.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Not all mcp servers are relevant for all clients

2 participants